前言
Tapable 库是一个很小的库,它为 Webpack 提供了一个核心能力,就是事件流机制。通过 Hook (钩子)能够让开发者轻松的访问到构建流程中的各个阶级和数据对象。本节将对 Tapable 库源码进行分析。
1、Tapable 库
在 Webpack 官方文档中这样介绍 Tapable 库:
1
| 这个小型库是 webpack 的一个核心工具,但也可用于其他地方, 以提供类似的插件接口。在 webpack 中的许多对象都扩展自 Tapable 类。 它对外暴露了 tap,tapAsync 和 tapPromise 等方法, 插件可以使用这些方法向 webpack 中注入自定义构建的步骤,这些步骤将在构建过程中触发。
|
Tap 有敲击\窃听的意思,一个词两个意思。因此,从名字上就很容易理解它的功能。本文使用到的版本为:
1.1、三大核心
这个库很小,仅16个文件,除了 index.js 之外,还有 15 个类文件。该库有三个核心:
- Tapable.js ,webpack 中很多类继承了该类,比如 compiler、compilation 等
- HookCodeFactory.js,针对不同的 hook,生成需要控制流程的函数代码。该库中很多 hook 文件中都有配套的工厂类继承于它
- Hook.js,不同类型钩子的基类,提供了基本的 tap 方法,不同类型的子类可以重写这些方法。而 call 方法则由上面的工厂类去实现
接下来,我们先看下 Hook 这个核心部分。毕竟所有的东西都围绕着 Hook 的 tap、call 方法进行流程的运转。
1.2、约定
本文中如无特殊说明,tap 指代 xxHook 的 tap\tapAsync\tapPromise 这些注册方法,call 指代 call\callAsync\promise 这些方法。
2、深入 Hook
2.1、Hook 的类型
每个 hook 都可以通过 tap 注册一个或多个函数。这些函数如何执行要看 hook 的类型:
- 基本类型(名字中没有 Waterfall、Bail 或 Loop)。这类 hook 会按照注册的顺序依次执行钩子函数。
- Waterfall 类型。也是依次调用注册的函数。不同于基本类型的是它会把函数的返回值传递给下一个函数。
- Bail 类型。它允许提前退出。当任意一个注册的函数有返回值(非undefined)时,则停止剩余函数的执行。
- Loop 类型。如果任意一个函数返回非 undefined 值则从头执行注册的函数。直到所有的函数都返回 undefined。
此外,hooks 可以是同步的也可以是异步的。反映在名字上,有 Sync、AsyncSeries、AsyncParallel 钩子类:
- Sync。同步钩子可以注册同步函数,使用 myHook.tap() 方法。
- AsyncSeries。异步串行钩子可以使用注册异步的,基于回调和基于 promise 的函数(使用 myHook.tap()、myHook.tapAsync() 和 myHook.tapPromise())。会依次调用每一个异步函数。
- AsyncParallel。异步并行钩子可以注册异步,基于回调和基于 promise 的函数(使用 myHook.tap()、myHook.tapAsync() 和 myHook.tapPromise())。然而它会并行的运行每个异步函数。
比如:AsyncSeriesWaterfallHook 可以注册异步函数,串行运行它们,并把函数的返回值传递给下一个函数。
2.2、tap 方法
上文提到了三种 tap 方法可以来注册钩子,在 webpack 中可以访问到各个执行阶段的数据。我们现在来看一下 Hooks.js 这个基类里 tap 的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| node_modules\tapable\lib\Hook.js class Hook { constructor(args) { if (!Array.isArray(args)) args = []; this._args = args; this.taps = []; this.interceptors = []; this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; this._x = undefined; } tap(options, fn) { options = Object.assign({ type: "sync", fn: fn }, options); options = this._runRegisterInterceptors(options); this._insert(options); } tapAsync(options, fn) { options = Object.assign({ type: "async", fn: fn }, options); options = this._runRegisterInterceptors(options); this._insert(options); } tapPromise(options, fn) { options = Object.assign({ type: "promise", fn: fn }, options); options = this._runRegisterInterceptors(options); this._insert(options); } _resetCompilation() { this.call = this._call; this.callAsync = this._callAsync; this.promise = this._promise; } _insert(item) { this._resetCompilation(); this.taps[i] = item; } }
|
这里我们看到了三个 tap 方法,唯一的不同就是 type,然后得到一个 options 对象。然后进行拦截器注册的处理,这里本文暂时忽略,最后来到就是执行 _insert 方法。该方法代码较多,删减之后保留了核心的两行,一个调用 _resetCompilation 进行 call 函数的重置,最后就是都放在了 taps 这个数组里面,中间省去的是确定注册函数插入的位置。
2.3、call 方法
在看过 tap 方法之后,完成了钩子函数的注册,call 方法正式登场。还是 Hook.js 这个类文件里的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| node_modules\tapable\lib\Hook.js class Hook { compile(options) { throw new Error("Abstract: should be overriden"); } _createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); } } function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; } Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate("call", "sync"), configurable: true, writable: true }, _promise: { value: createCompileDelegate("promise", "promise"), configurable: true, writable: true }, _callAsync: { value: createCompileDelegate("callAsync", "async"), configurable: true, writable: true } });
|
我们在 constructor 中看到了 this.callAsync = this._callAsync 。然后上面这段源码中,我们看到通过 Object.defineProperties 定义了 _callAsync 这个函数,它是通过 createCompileDelegate 生成了一个 lazyCompileHook 函数。里面进行了 this._createCall(type) 调用,这个方法继续调用了 Hook.compile 函数,这个函数是一个是由子类进行重写的一个函数。
2.4、示例:AsyncParallelHook
1 2 3 4 5 6 7 8
| class Compiler extends Tapable { constructor(context) { super(); this.hooks = { make: new AsyncParallelHook(["compilation"]) } } }
|
回到我们上一篇中,compiler.hooks.make.callAsync 这个方法,我们可以看到 make 这个 hook 的类型是 AsyncParallelHook。这个类也是 Hook 类的子类,我们来看一下这个类的源码部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| node_modules\tapable\lib\AsyncParallelHook.js class AsyncParallelHookCodeFactory extends HookCodeFactory { content({ onError, onDone }) { return this.callTapsParallel({ onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true), onDone }); } } const factory = new AsyncParallelHookCodeFactory(); class AsyncParallelHook extends Hook { compile(options) { factory.setup(this, options); return factory.create(options); } } Object.defineProperties(AsyncParallelHook.prototype, { _call: { value: undefined, configurable: true, writable: true } });
|
这里的源码不是很复杂,基本上做了下面几个事情:
- 重新设置了_call 属性
- 实现了 Hook 基类的 compile 方法,并调用工厂对象的 setup,返回 factory.create 函数的返回值
- 实现工厂类的 content 方法
这里涉及了工厂类的几个方法,因此我们需要看一下 HookCodeFactory 这个类源码的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| node_modules\tapable\lib\HookCodeFactory.js class HookCodeFactory { create(options) { this.init(options); let fn; switch (this.options.type) { case "sync": break; case "async": fn = new Function( this.args({ after: "_callback" }), '"use strict";\n' + this.header() + this.content({ onError: err => `_callback(${err});\n`, onResult: result => `_callback(null, ${result});\n`, onDone: () => "_callback();\n" }) ); break; case "promise": break; } this.deinit(); return fn; } setup(instance, options) { instance._x = options.taps.map(t => t.fn); } init(options) { this.options = options; this._args = options.args.slice(); } header() { let code = ""; code += "var _x = this._x;\n"; return code; } }
|
ok,经过几个类源码的分析,我们可以得出通过 Hook 的 tap 方法收集钩子函数,调用 Hook 的 call 方法,实际是执行工厂动态生成的控制函数。根据钩子的类型,就会有不同的控制函数。每一种 Hook 子类定义文件中都会有一个 HookCodeFactory 子类的定义来配合实现控制函数的生成。这里我们以一个示例代码,来看看到底生成的动态函数是什么样子的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| asyncParallel.js const {AsyncParallelHook} = require('tapable') const hooks = new AsyncParallelHook(['name']) hooks.tapAsync('h', (name, callback)=>{ console.log('h', name, this) callback() }) hooks.tapAsync('h1', (name, callback)=>{ console.log('h1', name) return callback() }) hooks.tap('h2', name=>{ console.log('h2', name) }) hooks.callAsync('robin', ()=>{ console.log('callAsync') }) node_modules\tapable\lib\Hook.js function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; } 生成的控制函数: (function anonymous(name, _callback) { "use strict"; var _context; var _x = this._x; do { var _counter = 3; var _done = () = >{ _callback(); }; if (_counter <= 0) break; var _fn0 = _x[0]; _fn0(name, _err0 = >{ if (_err0) { if (_counter > 0) { _callback(_err0); _counter = 0; } } else { if (--_counter === 0) _done(); } }); if (_counter <= 0) break; var _fn1 = _x[1]; _fn1(name, _err1 = >{ if (_err1) { if (_counter > 0) { _callback(_err1); _counter = 0; } } else { if (--_counter === 0) _done(); } }); if (_counter <= 0) break; var _fn2 = _x[2]; var _hasError2 = false; try { _fn2(name); } catch(_err) { _hasError2 = true; if (_counter > 0) { _callback(_err); _counter = 0; } } if (!_hasError2) { if (--_counter === 0) _done(); } } while ( false ); })
|
因为是异步并发 Hook 类型,所以钩子函数都是同步执行的,然后在钩子的回调里进行的流程控制,去计数钩子是否完成(无论是正常还是产生了 err)。最后才去执行了 _callback(),即我们在 hooks.callAsync 定义的箭头函数。这里有一个问题需要注意一下就是,我们看到 h 和 h1 我们使用的 tapAsync,而 h2 我们用的是 tap。因此,在控制上有一些差异,前面两个我们在控制函数中多了一个参数:回调函数,也就是 hooks.tapAsync(‘h1’, (name, callback)) 里的这个 callback,这个 callback 函数是要执行的,否则最后 hooks.callAsync 里的箭头函数是无法执行到的。
分析到这里,我们就了解到了 compiler.hooks.make.callAsync 是怎么执行到了 compilation.finish()。其他类型的 Hook,本文不再一一分析,留给读者去学习。下一节,我们继续讲解 Module 具体是如何构建的。